{% extends "console/layout.html" %}
{% block title %}Routing · {{site_name|default('RustPBX')}}{% endblock %}
{% block content %}
<div class="p-6">
    <div class="mx-auto max-w-7xl space-y-6" x-data='routingConsole({
        basePath: {{ base_path | tojson }},
        filters: {{ filters | tojson }},
        createUrl: {{ create_url | tojson }}
    })' x-init="init()">
        <div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
            <div>
                <h1 class="text-2xl font-semibold text-slate-900">Routing control</h1>
                <p class="mt-2 text-sm text-slate-500">Prioritise trunks, match calls with regex, and rewrite or
                    forward traffic based on your voice policies.</p>
            </div>
            <div class="flex flex-wrap gap-3">
                <button type="button"
                    class="inline-flex items-center gap-2 rounded-lg border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:border-sky-300 hover:text-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-200 focus:ring-offset-2"
                    :class="reloading ? 'cursor-wait border-slate-200 text-slate-400 hover:border-slate-200 hover:text-slate-400' : ''"
                    :disabled="reloading" @click="confirmReload()">
                    <template x-if="!reloading">
                        <svg class="h-4 w-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6">
                            <path stroke-linecap="round" stroke-linejoin="round"
                                d="M4.5 10a5.5 5.5 0 0 1 9.35-3.89l.65.65M15.5 10a5.5 5.5 0 0 1-9.35 3.89l-.65-.65M10 4.5V2m0 18v-2" />
                        </svg>
                    </template>
                    <template x-if="reloading">
                        <svg class="h-4 w-4 animate-spin text-slate-400" viewBox="0 0 24 24" fill="none"
                            stroke="currentColor" stroke-width="1.8">
                            <path stroke-linecap="round" stroke-linejoin="round"
                                d="M12 3v3m6.364 1.636-2.121 2.121M21 12h-3m-1.636 6.364-2.121-2.121M12 21v-3m-6.364-1.636 2.121-2.121M3 12h3m1.636-6.364 2.121 2.121" />
                        </svg>
                    </template>
                    Reload routes
                </button>
                <a :href="createUrl"
                    class="inline-flex items-center gap-2 rounded-lg bg-sky-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-400 focus:ring-offset-2">
                    <svg class="h-4 w-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M10 4v12m6-6H4" />
                    </svg>
                    New route
                </a>
            </div>
        </div>

        <section class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
            <div class="rounded-xl bg-white p-4 shadow-sm ring-1 ring-black/5">
                <div class="text-xs font-semibold uppercase tracking-wide text-slate-400">Total routes</div>
                <div class="mt-2 flex items-baseline gap-2">
                    <span class="text-2xl font-semibold text-slate-900" x-text="summary.total_routes"></span>
                    <span class="text-xs text-slate-500">rules</span>
                </div>
            </div>
            <div class="rounded-xl bg-white p-4 shadow-sm ring-1 ring-black/5">
                <div class="text-xs font-semibold uppercase tracking-wide text-slate-400">Active routes</div>
                <div class="mt-2 text-2xl font-semibold text-emerald-600" x-text="summary.active_routes"></div>
                <div class="text-xs text-slate-500">Enabled for real-time traffic</div>
            </div>
            <div class="rounded-xl bg-white p-4 shadow-sm ring-1 ring-black/5">
                <div class="text-xs font-semibold uppercase tracking-wide text-slate-400">Paused routes</div>
                <div class="mt-2 text-2xl font-semibold text-amber-600" x-text="pausedRoutes"></div>
                <div class="text-xs text-slate-500">Temporarily disabled for maintenance</div>
            </div>
            <div class="rounded-xl bg-white p-4 shadow-sm ring-1 ring-black/5">
                <div class="text-xs font-semibold uppercase tracking-wide text-slate-400">Last deploy</div>
                <div class="mt-2 text-base font-semibold text-slate-900" x-text="formatDate(summary.last_deploy)"></div>
                <div class="text-xs text-slate-500">Across the cluster</div>
            </div>
        </section>

        <template x-if="flash">
            <div class="rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700"
                x-text="flash"></div>
        </template>
        <template x-if="error">
            <div class="rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700" x-text="error">
            </div>
        </template>

        <template x-if="lastReload">
            <div class="rounded-lg border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
                <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                    <div>
                        <span class="font-semibold text-slate-800">Latest reload</span>
                        <span class="ml-2" x-text="buildReloadSummary(lastReload)"></span>
                    </div>
                    <div class="text-xs text-slate-500">
                        <span>Finished </span>
                        <span x-text="formatDate(lastReload.finished_at)"></span>
                    </div>
                </div>
                <div class="mt-2 text-xs text-slate-500">
                    Sources: config
                    <span class="font-semibold text-slate-700" x-text="Number(lastReload.config_count) || 0"></span>,
                    include files
                    <span class="font-semibold text-slate-700" x-text="Number(lastReload.file_count) || 0"></span>,
                    database
                    <span class="font-semibold text-slate-700" x-text="Number(lastReload.db_count) || 0"></span>.
                </div>
                <template x-if="lastReload.patterns && lastReload.patterns.length">
                    <div class="mt-2 text-xs text-slate-500">
                        <div class="font-semibold text-slate-600">Include patterns</div>
                        <ul class="mt-1 list-disc space-y-0.5 pl-5">
                            <template x-for="pattern in lastReload.patterns" :key="pattern">
                                <li class="break-all" x-text="pattern"></li>
                            </template>
                        </ul>
                    </div>
                </template>
                <template x-if="lastReload.files && lastReload.files.length">
                    <div class="mt-2 text-xs text-slate-500">
                        <div class="font-semibold text-slate-600">Include files</div>
                        <ul class="mt-1 list-disc space-y-0.5 pl-5">
                            <template x-for="file in lastReload.files" :key="file">
                                <li class="break-all" x-text="file"></li>
                            </template>
                        </ul>
                    </div>
                </template>
            </div>
        </template>

        <section class="rounded-xl bg-white p-5 shadow-sm ring-1 ring-black/5">
            <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
                <div>
                    <h2 class="text-base font-semibold text-slate-900">Routes</h2>
                    <p class="text-xs text-slate-500">Filter by direction, status, selection method, or search by
                        regex, trunk, or owner.</p>
                </div>
                <div class="flex flex-wrap items-center gap-3">
                    <div class="relative">
                        <input type="search" x-model.debounce.300ms="search" @input.debounce.400ms="applyFilters()"
                            class="w-72 rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-700 focus:border-sky-300 focus:outline-none focus:ring-2 focus:ring-sky-200"
                            placeholder="Search routes, regex, trunks...">
                        <svg class="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400"
                            viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6">
                            <path stroke-linecap="round" stroke-linejoin="round"
                                d="M12.5 12.5l4 4m-2.5-6a5.5 5.5 0 11-11 0 5.5 5.5 0 0111 0z" />
                        </svg>
                    </div>
                    <div class="flex gap-1 rounded-lg border border-slate-200 p-1 text-xs font-medium text-slate-600">
                        <button type="button" class="rounded-md px-3 py-1 transition"
                            :class="directionFilter === 'all' ? 'bg-sky-100 text-sky-700' : 'hover:bg-slate-100'"
                            @click="setDirection('all')">All</button>
                        <button type="button" class="rounded-md px-3 py-1 transition"
                            :class="directionFilter === 'inbound' ? 'bg-sky-100 text-sky-700' : 'hover:bg-slate-100'"
                            @click="setDirection('inbound')">Inbound</button>
                        <button type="button" class="rounded-md px-3 py-1 transition"
                            :class="directionFilter === 'outbound' ? 'bg-sky-100 text-sky-700' : 'hover:bg-slate-100'"
                            @click="setDirection('outbound')">Outbound</button>
                    </div>
                    <div class="flex gap-1 rounded-lg border border-slate-200 p-1 text-xs font-medium text-slate-600">
                        <button type="button" class="rounded-md px-3 py-1 transition"
                            :class="statusFilter === 'all' ? 'bg-emerald-100 text-emerald-700' : 'hover:bg-slate-100'"
                            @click="setStatus('all')">Any status</button>
                        <button type="button" class="rounded-md px-3 py-1 transition"
                            :class="statusFilter === 'active' ? 'bg-emerald-100 text-emerald-700' : 'hover:bg-slate-100'"
                            @click="setStatus('active')">Active</button>
                        <button type="button" class="rounded-md px-3 py-1 transition"
                            :class="statusFilter === 'disabled' ? 'bg-rose-100 text-rose-700' : 'hover:bg-slate-100'"
                            @click="setStatus('disabled')">Paused</button>
                    </div>
                    <div class="flex gap-1 text-xs">
                        <template x-for="option in algorithmOptions" :key="option.value">
                            <button type="button"
                                class="rounded-lg border border-slate-200 px-3 py-1 font-medium transition"
                                :class="algorithmFilter === option.value ? 'border-sky-300 bg-sky-50 text-sky-700' : 'hover:bg-slate-100'"
                                @click="setAlgorithm(option.value)">
                                <span x-text="option.label"></span>
                            </button>
                        </template>
                    </div>
                </div>
            </div>

            <div class="mt-5 divide-y divide-slate-100">
                <template x-if="loading">
                    <div
                        class="flex flex-col items-center justify-center gap-3 py-14 text-center text-sm text-slate-400">
                        <svg class="h-10 w-10 animate-spin text-slate-300" viewBox="0 0 24 24" fill="none"
                            stroke="currentColor" stroke-width="1.8">
                            <path stroke-linecap="round" stroke-linejoin="round"
                                d="M12 3v3m6.364 1.636-2.121 2.121M21 12h-3m-1.636 6.364-2.121-2.121M12 21v-3m-6.364-1.636 2.121-2.121M3 12h3m1.636-6.364 2.121 2.121" />
                        </svg>
                        <div>Loading routes…</div>
                    </div>
                </template>
                <template x-if="!loading && !filteredRoutes.length">
                    <div
                        class="flex flex-col items-center justify-center gap-3 py-14 text-center text-sm text-slate-400">
                        <svg class="h-10 w-10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
                            <path stroke-linecap="round" stroke-linejoin="round" d="M4 11h4l2-5 4 10 2-5h4" />
                            <path stroke-linecap="round" stroke-linejoin="round" d="M4 19h16" />
                        </svg>
                        <div>No routes found for the current filters.</div>
                    </div>
                </template>

                <template x-for="route in filteredRoutes" :key="route.id">
                    <article class="py-4" :class="route.disabled ? 'opacity-60' : ''" x-data="{ open: false }">
                        <header
                            class="flex flex-col gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3 shadow-sm transition hover:border-sky-200 hover:shadow md:flex-row md:items-center md:justify-between">
                            <div class="flex flex-1 flex-col gap-2">
                                <div class="flex flex-wrap items-center gap-2">
                                    <h3 class="text-base font-semibold text-slate-900">
                                        <a :href="detailUrl(route)"
                                            class="inline-flex items-center text-slate-900 transition hover:text-sky-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white"
                                            x-text="route.name"></a>
                                    </h3>
                                    <span
                                        class="inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-[11px] font-semibold"
                                        :class="route.direction === 'inbound' ? 'bg-emerald-50 text-emerald-600 ring-1 ring-emerald-200' : 'bg-sky-50 text-sky-600 ring-1 ring-sky-200'">
                                        <svg class="h-3 w-3" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                            stroke-width="2">
                                            <path x-show="route.direction === 'inbound'" stroke-linecap="round"
                                                stroke-linejoin="round" d="M5 8l5 5 5-5" />
                                            <path x-show="route.direction === 'outbound'" stroke-linecap="round"
                                                stroke-linejoin="round" d="M5 12l5-5 5 5" />
                                        </svg>
                                        <span class="uppercase" x-text="route.direction"></span>
                                    </span>
                                    <span
                                        class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2.5 py-0.5 text-[11px] font-medium text-slate-600 ring-1 ring-slate-200">
                                        Priority
                                        <span class="font-semibold" x-text="route.priority"></span>
                                    </span>
                                    <span
                                        class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2.5 py-0.5 text-[11px] font-medium text-slate-600 ring-1 ring-slate-200">
                                        <svg class="h-3 w-3" viewBox="0 0 20 20" fill="none" stroke="currentColor"
                                            stroke-width="1.6">
                                            <path stroke-linecap="round" stroke-linejoin="round"
                                                d="M6 6h8M6 10h5M6 14h8" />
                                        </svg>
                                        <span x-text="route.action.select.toUpperCase()"></span>
                                    </span>
                                    <template x-if="route.disabled">
                                        <span Alpine.data('routingConsole', (options={})=> ({
                                            basePath: '',
                                            listEndpoint: '',
                                            createUrl: '',
                                            filtersRaw: {},
                                            routes: [],
                                            summary: { total_routes: 0, active_routes: 0, last_deploy: null },
                                            pagination: null,
                                            loading: false,
                                            error: null,
                                            search: '',
                                            directionFilter: 'all',
                                            statusFilter: 'all',
                                            algorithmFilter: 'all',
                                            algorithmOptions: [],
                                            directionOptions: [],
                                            statusOptions: [],
                                            perPage: 20,
                                            page: 1,
                                            init() {
                                            this.basePath = this.normalizeBasePath(options.basePath);
                                            this.listEndpoint = this.resolvePath(`${this.basePath}/routing`);
                                            this.createUrl = options.createUrl ||
                                            this.resolvePath(`${this.basePath}/routing/new`);
                                            this.filtersRaw = options.filters || {};
                                            this.setupFilterOptions();
                                            this.fetchRoutes();
                                            },
                                            normalizeBasePath(path) {
                                            if (!path) {
                                            return '';
                                            }
                                            const trimmed = String(path).trim();
                                            if (!trimmed.length || trimmed === '/') {
                                            return '';
                                            }
                                            return trimmed.replace(/\/$/, '');
                                            },
                                            resolvePath(path) {
                                            const raw = (path || '').toString().trim();
                                            if (!raw.length) {
                                            return '/';
                                            }
                                            if (raw.startsWith('http://') || raw.startsWith('https://')) {
                                            return raw;
                                            }
                                            const normalized = raw.replace(/\/+ /g, '/');
                                            const withLeading = normalized.startsWith('/') ? normalized :
                                            `/${normalized}`;
                                            return withLeading.replace(/\/+/, '/');
                                            },
                                            setupFilterOptions() {
                                            const raw = this.filtersRaw || {};
                                            this.algorithmOptions =
                                            this.normalizeAlgorithmOptions(raw.selection_algorithms);
                                            if (!this.algorithmOptions.length) {
                                            this.algorithmOptions = [
                                            { value: 'rr', label: 'Round robin' },
                                            { value: 'weight', label: 'Weighted' },
                                            { value: 'hash', label: 'Deterministic hash' },
                                            ];
                                            }
                                            this.directionOptions =
                                            this.normalizeDirectionOptions(raw.direction_options);
                                            this.statusOptions = this.normalizeStatusOptions(raw.status_options);
                                            },
                                            normalizeAlgorithmOptions(input) {
                                            if (!Array.isArray(input)) {
                                            return [];
                                            }
                                            return input
                                            .map((item) => {
                                            if (item && typeof item === 'object') {
                                            const value = item.value ?? item.name ?? item.id ?? '';
                                            const label = item.label ?? item.title ?? value;
                                            if (!value || !label) return null;
                                            return { value: String(value), label: String(label) };
                                            }
                                            if (typeof item === 'string') {
                                            const pretty = item.replace(/_/g, ' ');
                                            return {
                                            value: item,
                                            label: pretty.charAt(0).toUpperCase() + pretty.slice(1),
                                            };
                                            }
                                            return null;
                                            })
                                            .filter(Boolean);
                                            },
                                            normalizeDirectionOptions(input) {
                                            if (!Array.isArray(input)) {
                                            return [];
                                            }
                                            return input
                                            .map((value) => (typeof value === 'string' && value.trim().length ?
                                            value.trim() : null))
                                            .filter(Boolean);
                                            },
                                            normalizeStatusOptions(input) {
                                            if (!Array.isArray(input)) {
                                            return [];
                                            }
                                            return input
                                            .map((item) => {
                                            if (item && typeof item === 'object') {
                                            return {
                                            value: item.value,
                                            label: item.label,
                                            };
                                            }
                                            return null;
                                            })
                                            .filter((entry) => entry && entry.value !== undefined && entry.label);
                                            },
                                            buildPayload() {
                                            const filters = {};
                                            const term = this.search.trim();
                                            if (term.length) {
                                            filters.q = term;
                                            }
                                            if (this.directionFilter !== 'all') {
                                            filters.direction = this.directionFilter;
                                            }
                                            if (this.statusFilter !== 'all') {
                                            filters.status = this.statusFilter === 'disabled' ? 'paused' :
                                            this.statusFilter;
                                            }
                                            if (this.algorithmFilter !== 'all') {
                                            filters.selection = this.algorithmFilter;
                                            }

                                            return {
                                            page: this.page,
                                            per_page: this.perPage,
                                            filters,
                                            };
                                            },
                                            async fetchRoutes() {
                                            this.loading = true;
                                            this.error = null;
                                            try {
                                            const response = await fetch(this.listEndpoint, {
                                            method: 'POST',
                                            headers: {
                                            'Content-Type': 'application/json',
                                            'Accept': 'application/json',
                                            },
                                            body: JSON.stringify(this.buildPayload()),
                                            });
                                            const data = await response.json().catch(() => ({}));
                                            if (!response.ok) {
                                            throw new Error(data?.message || 'Failed to load routing rules');
                                            }

                                            this.routes = Array.isArray(data?.items) ? data.items : [];
                                            this.summary = Object.assign(
                                            { total_routes: 0, active_routes: 0, last_deploy: null },
                                            data?.summary || {}
                                            );
                                            this.updateFiltersFromResponse(data?.filters);
                                            this.updatePagination(data);
                                            } catch (err) {
                                            console.error(err);
                                            this.error = err?.message || 'Unable to load routing rules';
                                            } finally {
                                            this.loading = false;
                                            }
                                            },
                                            updateFiltersFromResponse(filters) {
                                            if (!filters || typeof filters !== 'object') {
                                            return;
                                            }
                                            this.filtersRaw = filters;
                                            this.setupFilterOptions();
                                            },
                                            updatePagination(meta) {
                                            const perPageRaw = Number(meta?.per_page);
                                            const currentPageRaw = Number(meta?.page);
                                            const totalItemsRaw = Number(meta?.total_items);
                                            const totalPagesRaw = Number(meta?.total_pages);

                                            const perPage = Number.isFinite(perPageRaw) && perPageRaw > 0 ? perPageRaw :
                                            this.perPage;
                                            const totalItems = Number.isFinite(totalItemsRaw) && totalItemsRaw >= 0
                                            ? totalItemsRaw
                                            : this.routes.length;
                                            const inferredPages = Math.max(Math.ceil(totalItems / perPage), 1);
                                            const totalPages = Number.isFinite(totalPagesRaw) && totalPagesRaw >= 1
                                            ? Math.max(Math.min(totalPagesRaw, inferredPages || 1), 1)
                                            : inferredPages;
                                            const currentPage = Number.isFinite(currentPageRaw) && currentPageRaw > 0
                                            ? Math.min(currentPageRaw, totalPages)
                                            : Math.min(this.page || 1, totalPages);

                                            const resultsCount = this.routes.length;
                                            const showingFrom = resultsCount ? ((currentPage - 1) * perPage) + 1 : 0;
                                            const showingTo = resultsCount ? Math.min(showingFrom + resultsCount - 1,
                                            totalItems) : 0;

                                            this.pagination = {
                                            current_page: currentPage,
                                            per_page: perPage,
                                            total_items: totalItems,
                                            total_pages: totalPages,
                                            has_prev: currentPage > 1,
                                            has_next: currentPage < totalPages, prev_page: currentPage> 1 ? currentPage
                                                - 1 : null,
                                                next_page: currentPage < totalPages ? currentPage + 1 : null,
                                                    showing_from: showingFrom, showing_to: showingTo, };
                                                    this.perPage=perPage; this.page=currentPage; }, applyFilters() {
                                                    this.page=1; this.fetchRoutes(); }, setDirection(value) { if
                                                    (this.directionFilter===value) { if (value !=='all' ) {
                                                    this.directionFilter='all' ; this.applyFilters(); } return; }
                                                    this.directionFilter=value; this.applyFilters(); }, setStatus(value)
                                                    { if (this.statusFilter===value) { if (value !=='all' ) {
                                                    this.statusFilter='all' ; this.applyFilters(); } return; }
                                                    this.statusFilter=value; this.applyFilters(); }, setAlgorithm(value)
                                                    { this.algorithmFilter=this.algorithmFilter===value ? 'all' : value;
                                                    this.applyFilters(); }, get filteredRoutes() { return
                                                    Array.isArray(this.routes) ? this.routes : []; }, get pausedRoutes()
                                                    { const total=Number(this.summary.total_routes) || 0; const
                                                    active=Number(this.summary.active_routes) || 0; return
                                                    Math.max(total - active, 0); }, goToPage(target) { if
                                                    (!this.pagination) return; const
                                                    totalPages=this.pagination.total_pages || 1; const
                                                    page=Math.min(Math.max(target, 1), totalPages); if
                                                    (page===this.page) return; this.page=page; this.fetchRoutes(); },
                                                    prevPage() { if (this.pagination?.has_prev) {
                                                    this.goToPage(this.pagination.prev_page || (this.page - 1)); } },
                                                    nextPage() { if (this.pagination?.has_next) {
                                                    this.goToPage(this.pagination.next_page || (this.page + 1)); } },
                                                    formatDate(value) { if (!value) return '—' ; const date=new
                                                    Date(value); if (Number.isNaN(date.getTime())) { return value; }
                                                    return date.toLocaleString(); }, matchEntries(route) {
                                                    class="mt-0.5 inline-flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-amber-50 text-[11px] font-semibold text-amber-600 ring-1 ring-amber-100">
                                                    ↺
                                        </span>
                                        <div>
                                            <div class="text-xs font-semibold text-slate-500" x-text="item.label"></div>
                                            <code class="text-xs text-slate-600" x-text="item.value"></code>
                                        </div>
                                        </li>
                                    </template>
                                    <template x-if="!rewriteEntries(route).length">
                                        <li class="text-xs text-slate-400">No rewrite applied</li>
                                    </template>
                                    </ul>
                                </div>
                            </div>

                            <div class="grid gap-4 md:grid-cols-2">
                                <div class="rounded-lg border border-slate-200 p-4">
                                    <div class="mb-3 flex items-center justify-between">
                                        <div>
                                            <div class="text-xs font-semibold uppercase tracking-wide text-slate-400">
                                                Trunk selection</div>
                                            <div class="text-sm font-semibold text-slate-700"
                                                x-text="selectionLabel(route)"></div>
                                            <template x-if="route.action.hash_key">
                                                <div class="text-xs text-slate-500">Hash key: <span class="font-mono"
                                                        x-text="route.action.hash_key"></span></div>
                                            </template>
                                        </div>
                                        <div
                                            class="rounded-full bg-slate-100 px-3 py-1 text-[11px] font-semibold text-slate-600">
                                            <span x-text="totalWeight(route)"></span>
                                            total weight
                                        </div>
                                    </div>
                                    <div class="space-y-2">
                                        <template x-for="trunk in (route.action.trunks || [])" :key="trunk.name">
                                            <div class="rounded-lg border border-slate-200 px-3 py-2">
                                                <div
                                                    class="flex items-center justify-between text-xs font-medium text-slate-600">
                                                    <div class="flex items-center gap-2">
                                                        <span
                                                            class="inline-flex h-6 w-6 items-center justify-center rounded-full bg-slate-100 text-[11px] font-semibold text-slate-700">
                                                            <span x-text="trunkInitial(trunk.name)"></span>
                                                        </span>
                                                        <span x-text="trunk.name"></span>
                                                    </div>
                                                    <div class="flex items-center gap-2">
                                                        <span class="font-semibold text-slate-900"
                                                            x-text="trunk.weight ?? '—'"></span>
                                                        <span class="text-slate-400">wt</span>
                                                        <span
                                                            class="rounded-full bg-sky-50 px-2 py-0.5 text-[10px] font-semibold text-sky-600"
                                                            x-text="weightPercent(route, trunk) + '%' "></span>
                                                    </div>
                                                </div>
                                                <div class="mt-2 h-1.5 w-full rounded-full bg-slate-100">
                                                    <div class="h-full rounded-full bg-sky-500"
                                                        :style="{ width: weightPercent(route, trunk) + '%' }"></div>
                                                </div>
                                            </div>
                                        </template>
                                        <template x-if="!(route.action.trunks || []).length">
                                            <div
                                                class="rounded-lg border border-slate-200 px-3 py-2 text-xs text-slate-400">
                                                Uses default
                                                destination</div>
                                        </template>
                                    </div>
                                </div>
                                <div class="rounded-lg border border-slate-200 p-4">
                                    <div class="text-xs font-semibold uppercase tracking-wide text-slate-400">Source &
                                        target trunks</div>
                                    <div class="mt-3 grid gap-3 text-sm">
                                        <div>
                                            <div class="text-xs font-semibold text-slate-500">Source trunk</div>
                                            <div
                                                class="mt-1 inline-flex items-center gap-2 rounded-lg bg-slate-100 px-3 py-1 text-sm font-medium text-slate-700">
                                                <svg class="h-4 w-4" viewBox="0 0 20 20" fill="none"
                                                    stroke="currentColor" stroke-width="1.6">
                                                    <path stroke-linecap="round" stroke-linejoin="round"
                                                        d="M4 4h12v2H4zM4 9h12v2H4zM4 14h12v2H4z" />
                                                </svg>
                                                <span x-text="route.source_trunk || '—'"></span>
                                            </div>
                                        </div>
                                        <div>
                                            <div class="text-xs font-semibold text-slate-500">Target trunks</div>
                                            <div class="mt-1 flex flex-wrap gap-2">
                                                <template x-for="target in (route.target_trunks || [])" :key="target">
                                                    <span
                                                        class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-600">
                                                        <svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="none"
                                                            stroke="currentColor" stroke-width="1.6">
                                                            <path stroke-linecap="round" stroke-linejoin="round"
                                                                d="M5 10h10M12 5l5 5-5 5" />
                                                        </svg>
                                                        <span x-text="target"></span>
                                                    </span>
                                                </template>
                                                <template x-if="!(route.target_trunks || []).length">
                                                    <span class="text-xs text-slate-400">Default destination</span>
                                                </template>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </div>

                            <div class="rounded-lg border border-dashed border-slate-200 p-4">
                                <div class="text-xs font-semibold uppercase tracking-wide text-slate-400">Notes</div>
                                <ul class="mt-3 list-disc space-y-1 pl-5 text-xs text-slate-500">
                                    <template x-for="note in (route.notes || [])" :key="note">
                                        <li x-text="note"></li>
                                    </template>
                                    <template x-if="!(route.notes || []).length">
                                        <li>No operator notes recorded.</li>
                                    </template>
                                </ul>
                            </div>
            </div>
            </article>
            </template>
    </div>
    </section>

</div>
</div>

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('routingConsole', (options = {}) => ({
            basePath: '',
            listEndpoint: '',
            createUrl: '',
            amiEndpoint: options.amiEndpoint || '/ami/v1',
            filtersRaw: {},
            routes: [],
            summary: { total_routes: 0, active_routes: 0, last_deploy: null },
            pagination: null,
            loading: false,
            error: null,
            flash: null,
            search: '',
            directionFilter: 'all',
            statusFilter: 'all',
            algorithmFilter: 'all',
            algorithmOptions: [],
            directionOptions: [],
            statusOptions: [],
            perPage: 20,
            page: 1,
            reloading: false,
            lastReload: null,

            init() {
                this.basePath = this.normalizeBasePath(options.basePath);
                this.listEndpoint = this.resolvePath(`${this.basePath}/routing`);
                this.createUrl = options.createUrl || this.resolvePath(`${this.basePath}/routing/new`);
                this.filtersRaw = options.filters || {};
                this.setupFilterOptions();
                this.fetchRoutes();
            },
            normalizeBasePath(path) {
                if (!path) return '';
                const trimmed = String(path).trim();
                if (!trimmed.length || trimmed === '/') {
                    return '';
                }
                return trimmed.replace(/\/+$/, '');
            },
            resolvePath(path) {
                if (!path) return '/';
                const raw = String(path).trim();
                if (!raw.length) return '/';
                if (raw.startsWith('http://') || raw.startsWith('https://')) {
                    return raw;
                }
                const collapsed = raw.replace(/\/{2,}/g, '/');
                return collapsed.startsWith('/') ? collapsed : `/${collapsed}`;
            },
            setupFilterOptions() {
                const raw = this.filtersRaw || {};
                this.algorithmOptions = this.normalizeAlgorithmOptions(raw.selection_algorithms);
                if (!this.algorithmOptions.length) {
                    this.algorithmOptions = [
                        { value: 'rr', label: 'Round robin' },
                        { value: 'weight', label: 'Weighted' },
                        { value: 'hash', label: 'Deterministic hash' },
                    ];
                }
                this.directionOptions = this.normalizeDirectionOptions(raw.direction_options);
                this.statusOptions = this.normalizeStatusOptions(raw.status_options);
            },
            normalizeAlgorithmOptions(input) {
                if (!Array.isArray(input)) {
                    return [];
                }
                return input
                    .map((item) => {
                        if (item && typeof item === 'object') {
                            const value = item.value ?? item.name ?? item.id ?? '';
                            const label = item.label ?? item.title ?? value;
                            if (!value || !label) return null;
                            return { value: String(value), label: String(label) };
                        }
                        if (typeof item === 'string') {
                            const pretty = item.replace(/_/g, ' ');
                            return {
                                value: item,
                                label: pretty.charAt(0).toUpperCase() + pretty.slice(1),
                            };
                        }
                        return null;
                    })
                    .filter(Boolean);
            },
            normalizeDirectionOptions(input) {
                if (!Array.isArray(input)) {
                    return [];
                }
                return input
                    .map((value) => (typeof value === 'string' && value.trim().length ? value.trim() : null))
                    .filter(Boolean);
            },
            normalizeStatusOptions(input) {
                if (!Array.isArray(input)) {
                    return [];
                }
                return input
                    .map((item) => {
                        if (item && typeof item === 'object') {
                            return {
                                value: item.value,
                                label: item.label,
                            };
                        }
                        return null;
                    })
                    .filter((entry) => entry && entry.value !== undefined && entry.label);
            },
            buildPayload() {
                const filters = {};
                const term = this.search.trim();
                if (term.length) {
                    filters.q = term;
                }
                if (this.directionFilter !== 'all') {
                    filters.direction = this.directionFilter;
                }
                if (this.statusFilter !== 'all') {
                    filters.status = this.statusFilter === 'disabled' ? 'paused' : this.statusFilter;
                }
                if (this.algorithmFilter !== 'all') {
                    filters.selection = this.algorithmFilter;
                }

                return {
                    page: this.page,
                    per_page: this.perPage,
                    filters,
                };
            },
            async fetchRoutes() {
                this.loading = true;
                this.error = null;
                try {
                    const response = await fetch(this.listEndpoint, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'Accept': 'application/json',
                        },
                        body: JSON.stringify(this.buildPayload()),
                    });
                    const data = await response.json().catch(() => ({}));
                    if (!response.ok) {
                        throw new Error(data?.message || 'Failed to load routing rules');
                    }

                    const items = Array.isArray(data?.items) ? data.items : [];
                    this.routes = items.map((item) => ({
                        ...item,
                        disabled: Boolean(item?.disabled),
                        direction: item?.direction || 'outbound',
                        action: item?.action || {},
                    }));

                    this.summary = Object.assign(
                        { total_routes: 0, active_routes: 0, last_deploy: null },
                        data?.summary || {}
                    );
                    this.updateFiltersFromResponse(data?.filters);
                    this.updatePagination(data);
                } catch (err) {
                    console.error(err);
                    this.error = err?.message || 'Unable to load routing rules';
                } finally {
                    this.loading = false;
                }
            },
            updateFiltersFromResponse(filters) {
                if (!filters || typeof filters !== 'object') {
                    return;
                }
                this.filtersRaw = filters;
                this.setupFilterOptions();
            },
            updatePagination(meta) {
                const perPageRaw = Number(meta?.per_page);
                const currentPageRaw = Number(meta?.page);
                const totalItemsRaw = Number(meta?.total_items);
                const totalPagesRaw = Number(meta?.total_pages);

                const perPage = Number.isFinite(perPageRaw) && perPageRaw > 0 ? perPageRaw : this.perPage;
                const totalItems = Number.isFinite(totalItemsRaw) && totalItemsRaw >= 0
                    ? totalItemsRaw
                    : this.routes.length;
                const inferredPages = Math.max(Math.ceil(totalItems / perPage), 1);
                const totalPages = Number.isFinite(totalPagesRaw) && totalPagesRaw >= 1
                    ? Math.max(Math.min(totalPagesRaw, inferredPages || 1), 1)
                    : inferredPages;
                const currentPage = Number.isFinite(currentPageRaw) && currentPageRaw > 0
                    ? Math.min(currentPageRaw, totalPages)
                    : Math.min(this.page || 1, totalPages);

                const resultsCount = this.routes.length;
                const showingFrom = resultsCount ? ((currentPage - 1) * perPage) + 1 : 0;
                const showingTo = resultsCount ? Math.min(showingFrom + resultsCount - 1, totalItems) : 0;

                this.pagination = {
                    current_page: currentPage,
                    per_page: perPage,
                    total_items: totalItems,
                    total_pages: totalPages,
                    has_prev: currentPage > 1,
                    has_next: currentPage < totalPages,
                    prev_page: currentPage > 1 ? currentPage - 1 : null,
                    next_page: currentPage < totalPages ? currentPage + 1 : null,
                    showing_from: showingFrom,
                    showing_to: showingTo,
                };

                this.perPage = perPage;
                this.page = currentPage;
            },
            applyFilters() {
                this.page = 1;
                this.fetchRoutes();
            },
            setDirection(value) {
                if (this.directionFilter === value) {
                    if (value !== 'all') {
                        this.directionFilter = 'all';
                        this.applyFilters();
                    }
                    return;
                }
                this.directionFilter = value;
                this.applyFilters();
            },
            setStatus(value) {
                if (this.statusFilter === value) {
                    if (value !== 'all') {
                        this.statusFilter = 'all';
                        this.applyFilters();
                    }
                    return;
                }
                this.statusFilter = value;
                this.applyFilters();
            },
            setAlgorithm(value) {
                this.algorithmFilter = this.algorithmFilter === value ? 'all' : value;
                this.applyFilters();
            },
            get filteredRoutes() {
                return Array.isArray(this.routes) ? this.routes : [];
            },
            get pausedRoutes() {
                const total = Number(this.summary.total_routes) || 0;
                const active = Number(this.summary.active_routes) || 0;
                return Math.max(total - active, 0);
            },
            goToPage(target) {
                if (!this.pagination) return;
                const totalPages = this.pagination.total_pages || 1;
                const page = Math.min(Math.max(target, 1), totalPages);
                if (page === this.page) return;
                this.page = page;
                this.fetchRoutes();
            },
            prevPage() {
                if (this.pagination?.has_prev) {
                    this.goToPage(this.pagination.prev_page || (this.page - 1));
                }
            },
            nextPage() {
                if (this.pagination?.has_next) {
                    this.goToPage(this.pagination.next_page || (this.page + 1));
                }
            },
            formatDate(value) {
                if (!value) return '—';
                const date = new Date(value);
                if (Number.isNaN(date.getTime())) {
                    return value;
                }
                return date.toLocaleString();
            },
            formatDuration(milliseconds) {
                const value = Number(milliseconds);
                if (!Number.isFinite(value) || value < 0) {
                    return null;
                }
                if (value >= 1000) {
                    const seconds = value / 1000;
                    if (seconds >= 10) {
                        return `${Math.round(seconds)}s`;
                    }
                    return `${seconds.toFixed(1)}s`;
                }
                return `${Math.round(value)}ms`;
            },
            normalizeReloadMetrics(raw) {
                if (!raw || typeof raw !== 'object') {
                    return null;
                }
                const toCount = (input) => {
                    const value = Number(input);
                    return Number.isFinite(value) && value >= 0 ? value : 0;
                };
                const durationRaw = Number(raw.duration_ms);
                return {
                    total: toCount(raw.total),
                    config_count: toCount(raw.config_count),
                    file_count: toCount(raw.file_count),
                    db_count: toCount(raw.db_count),
                    duration_ms: Number.isFinite(durationRaw) ? durationRaw : null,
                    started_at: raw.started_at || null,
                    finished_at: raw.finished_at || null,
                    files: Array.isArray(raw.files) ? raw.files.map((item) => String(item)) : [],
                    patterns: Array.isArray(raw.patterns) ? raw.patterns.map((item) => String(item)) : [],
                };
            },
            buildReloadSummary(details) {
                if (!details || typeof details !== 'object') {
                    return 'Reload complete.';
                }
                const total = Number(details.total);
                const duration = this.formatDuration(details.duration_ms);
                const totalText = Number.isFinite(total) && total >= 0
                    ? `${total} routing rule${total === 1 ? '' : 's'}`
                    : null;
                if (totalText && duration) {
                    return `${totalText} in ${duration}`;
                }
                if (totalText) {
                    return totalText;
                }
                if (duration) {
                    return `Completed in ${duration}`;
                }
                return 'Reload complete.';
            },
            matchEntries(route) {
                const labels = {
                    'from_user': 'From user regex',
                    'from_host': 'From host regex',
                    'to_user': 'To user regex',
                    'to_host': 'To host regex',
                    'to_port': 'To port',
                    'request_uri_user': 'R-URI user regex',
                    'request_uri_host': 'R-URI host regex',
                    'request_uri_port': 'R-URI port',
                    'from': 'From',
                    'to': 'To',
                    'caller': 'Caller',
                    'callee': 'Callee',
                };
                const match = route.match || {};
                const entries = [];
                Object.entries(match).forEach(([key, value]) => {
                    if (value === null || value === undefined || value === '') {
                        return;
                    }
                    if (key === 'headers' && typeof value === 'object') {
                        Object.entries(value).forEach(([headerKey, headerValue]) => {
                            entries.push({
                                key: headerKey,
                                label: `Header ${headerKey.replace(/^header\./, '')}`,
                                value: headerValue,
                            });
                        });
                    } else {
                        entries.push({
                            key,
                            label: labels[key] || key,
                            value,
                        });
                    }
                });
                return entries;
            },
            rewriteEntries(route) {
                const labels = {
                    'from_user': 'Rewrite From user',
                    'from_host': 'Rewrite From host',
                    'to_user': 'Rewrite To user',
                    'to_host': 'Rewrite To host',
                    'to_port': 'Rewrite To port',
                    'request_uri_user': 'Rewrite R-URI user',
                    'request_uri_host': 'Rewrite R-URI host',
                    'request_uri_port': 'Rewrite R-URI port',
                    'from': 'Rewrite From',
                    'to': 'Rewrite To',
                    'caller': 'Rewrite Caller',
                    'callee': 'Rewrite Callee',
                };
                const rewrite = route.rewrite || {};
                const entries = [];
                Object.entries(rewrite).forEach(([key, value]) => {
                    if (value === null || value === undefined || value === '') {
                        return;
                    }
                    if (key === 'headers' && typeof value === 'object') {
                        Object.entries(value).forEach(([headerKey, headerValue]) => {
                            entries.push({
                                key: headerKey,
                                label: `Set header ${headerKey.replace(/^header\./, '')}`,
                                value: headerValue,
                            });
                        });
                    } else {
                        entries.push({
                            key,
                            label: labels[key] || key,
                            value,
                        });
                    }
                });
                return entries;
            },
            weightPercent(route, trunk) {
                const trunks = route.action?.trunks || [];
                const total = trunks.reduce((sum, item) => sum + (item.weight || 0), 0) || 1;
                const current = trunk.weight || 0;
                return Math.round((current / total) * 100);
            },
            totalWeight(route) {
                const trunks = route.action?.trunks || [];
                if (!trunks.length) return '—';
                const total = trunks.reduce((sum, item) => sum + (item.weight || 0), 0);
                return total || '—';
            },
            selectionLabel(route) {
                const select = (route.action?.select || 'rr').toLowerCase();
                const map = {
                    rr: 'Round robin balancing',
                    weight: 'Weighted distribution',
                    hash: 'Deterministic hash-based',
                };
                return map[select] || select;
            },
            detailUrl(route) {
                if (!route) {
                    return this.resolvePath(`${this.basePath}/routing`);
                }
                if (typeof route.detail_url === 'string' && route.detail_url.trim().length) {
                    return this.resolvePath(route.detail_url);
                }
                if (typeof route.edit_url === 'string' && route.edit_url.trim().length) {
                    return this.resolvePath(route.edit_url);
                }
                if (route.id !== undefined && route.id !== null) {
                    return this.resolvePath(`${this.basePath}/routing/${route.id}`);
                }
                return this.resolvePath(`${this.basePath}/routing`);
            },
            trunkInitial(name) {
                if (!name) return '?';
                return name
                    .split(/[^a-zA-Z0-9]/)
                    .filter(Boolean)
                    .map((part) => part[0]?.toUpperCase())
                    .join('')
                    .slice(0, 2);
            },
            confirmReload() {
                if (this.reloading) {
                    return;
                }
                window.dispatchEvent(new CustomEvent('console:confirm', {
                    detail: {
                        title: 'Reload routing rules',
                        message: 'Reload routing definitions from configuration and database? Existing calls will continue but new transactions use updated rules.',
                        confirmLabel: 'Reload',
                        cancelLabel: 'Cancel',
                        destructive: false,
                        onConfirm: () => this.reloadRoutes(),
                    },
                }));
            },
            async reloadRoutes() {
                if (this.reloading) {
                    return;
                }
                this.reloading = true;
                this.error = null;
                const endpoint = `${this.amiEndpoint.replace(/\/$/, '')}/reload/routes`;
                let successMessage = null;
                try {
                    const response = await fetch(endpoint, {
                        method: 'POST',
                        headers: {
                            'Accept': 'application/json',
                        },
                        credentials: 'include',
                    });
                    const data = await response.json().catch(() => ({}));
                    if (!response.ok) {
                        throw new Error(data?.message || data?.error || 'Failed to reload routing rules');
                    }
                    const count = Number.isFinite(Number(data?.routes_reloaded)) ? Number(data.routes_reloaded) : null;
                    const metrics = this.normalizeReloadMetrics(data?.metrics);
                    this.lastReload = metrics;
                    if (metrics && count !== null) {
                        const durationText = this.formatDuration(metrics.duration_ms);
                        if (durationText) {
                            successMessage = `Reloaded ${count} routing rule${count === 1 ? '' : 's'} in ${durationText}.`;
                        }
                    }
                    if (!successMessage) {
                        successMessage = count !== null
                            ? `Reloaded ${count} routing rule${count === 1 ? '' : 's'}.`
                            : 'Routing rules reloaded.';
                    }
                    await this.fetchRoutes();
                    this.flash = successMessage;
                } catch (err) {
                    console.error(err);
                    this.error = err?.message || 'Failed to reload routing rules';
                } finally {
                    this.reloading = false;
                }
            },
        }));
    });
</script>
{% endblock %}